const os = require('os');
const fs = require("fs")
const path = require("path");
var dgram=require('dgram');
const crypto = require('crypto');
const { exit } = require('process');
const http = require("http")
process.stdin.setEncoding('utf8');

// 账号
let USERNAME = ""
// 密码
let USERPASS = ""
// 认证服务器地址
let HOST_IP = ""
// 认证主机物理网卡信息
let MAC_ADDR = ""
// 认证主机ip
let LOCAL_IP = ""
// 服务类型，默认为internet
let SERVICE_TYPE = "internet"
// dhcp(自动获取)标志，默认为0
let DHCP_SETTING = "0";
// 小蝴蝶版本号，默认为3.8.2
let CLIENT_VERSION = "3.8.2"
// 是否允许重连标志，true为允许，false为不允许
let RECONNECT_ENABLE = true
let see;
// 本机的所有ip
let IP = [];


// 请求下线时会用到
let index = 0x01000000
let block = Buffer.from([0x2a, 0x06, 0, 0, 0, 0, 0x2b, 0x06, 0, 0, 0, 0, 0x2c, 0x06, 0, 0, 0, 0, 0x2d, 0x06,
    0, 0, 0, 0, 0x2e, 0x06, 0, 0, 0, 0, 0x2f, 0x06, 0, 0, 0, 0]); 

try{
    var udpSocket = dgram.createSocket("udp4");
    // udpSocket.bind(3848);
}catch(err){
    console.log(err.message)
    throw new Error("程序初始化失败！请检查端口(3848)是否被占用？");
}


main()
async function main(){
    // 读取配置信息
    let config = require(path.resolve(__dirname,"config.json"));
    HOST_IP = config.server_ip
    USERNAME = config.username
    USERPASS = config.password
    RECONNECT_ENABLE = config.reconnect_enable
    CLIENT_VERSION = config.client_version
    getLoad();

    console.log("-----------------------------------");
    console.log("  服务类型:    \t" + SERVICE_TYPE);
    console.log("  本地ip地址:  \t" + LOCAL_IP);
    console.log("  物理网卡地址:\t" + MAC_ADDR);
    console.log("  服务器ip地址:\t" + HOST_IP);
    console.log("  账户:      \t"  +  USERNAME);
    console.log("-----------------------------------");
    console.log("  如果10秒内无反应,请检测自身网络");
    console.log("  如网络并无问题,请重启程序尝试");
    console.log("-----------------------------------\n");
    run();
    // 初始化完成，开始正式运行
    while(true){
        let key = await readlineSync();
        if(key === "1"){
            if(!see){ 
                console.log("并未登录，无需下线");
            }else{
                // 进行下线
                console.log("开始下线");
                logout(see).then(e=>{
                    console.log("下线成功");
                    exit(0)
                })
            }
        }
    }
}

// 开始运行
async function run(){
    do{
        // 创建连接阻塞
        do{
            var session = "";
            await new Promise((data)=>{
                // 开始申请登录
                console.log("开始申请登录")
                login().then(e=>{
                    see = session = e;
                    data()
                });
            })
        }while(!session)
        // 开始执行心跳
        await breathe(session)
        // 到这里说明心跳不行了，进行下线
        await logout(see).then(e=>{ console.log("下线成功"); })
        // 等待1秒进行登录
        await new Promise(ress=>{ setTimeout(()=>{ress()},1000)})
    }while(RECONNECT_ENABLE)
    console.log("彻底退出，等待下线")
}


// 连接到internet，线程阻塞。当连接出错或超时，即status=-1或0时，方法执行结束
function login(){
    return new Promise((data,err)=>{
        try{
            // 创建连接认证的packet凭证
            let packet = generateLoginPacket();
            // 监控
            udpSocket.once("message",(msg, rinfo)=>{
                // 判断消息是不是认证服务器发送过来的
                if(HOST_IP !== rinfo.address)return
                // 解密数据
                let recvPacket = decrypt(msg)
                // md5数据
                let recvMd5 = Buffer.alloc(16)
                for (let j = 2; j < 18; j++) {
                    recvMd5[j - 2] = recvPacket[j];
                    recvPacket[j] = 0;
                }

                if(checkMD5(recvMd5,getMD5Bytes(recvPacket))){
                    let statu = recvPacket[20]; // 记录状态,0 或 1， 0表示不在线，1表示在线
                    
                    // 解析数据
                    let sessionLen = Number(recvPacket[22]);
                    let session = Buffer.alloc(sessionLen);

                    for (let i = 0, j = 23; j < sessionLen + 23; i++, j++) {
                        session[i] = recvPacket[j];
                        recvPacket[j] = 0;
                    }
                    let messageIndex = -1;
                    for (let j = sessionLen + 23; j < recvPacket.length; j++) {
                        if (recvPacket[j] == 11) {
                            messageIndex = j;
                            break;
                        }
                    }

                    let messageLen = recvPacket[Number(messageIndex) + 1] & 0xff;
                    let message = Buffer.alloc(messageLen);

                    let inde = 0;
                    for(let i=messageIndex + 2;i<messageIndex + 2 + messageLen;i++) message[inde++] = recvPacket[i]

                    if(gbk(message) === "不允许NAT后认证"){
                        // 换一个ip
                        if(IP.length === 0){
                            console.log("系统提示："+ gbk(message));
                            console.log("=====================================================================")
                            console.log("所有ip都已尝试登录，但都失败");
                            console.log("请检测自身网络是否直接连接学校网线,不可连接他人热点进行登录")
                            console.log("因他人热点分配给你的ip不是学校分配的,就会被识别为非法网络，不允许登录")
                            console.log("=====================================================================")
                            exit(0)
                        }
                        LOCAL_IP = IP[0].ip;
                        MAC_ADDR = IP[0].mac;  
                        IP.splice(0,1)
                    }else{
                        // 提示登录成功语句
                        console.log(gbk(message));
                    }

                    if(statu === 1){
                        //登录成功
                        data(session);
                    }else{
                        // 登录失败
                        data()
                    }
                }else {
                    console.log("md5校验出错")
                    data()
                }
            })
            // 进行登录
            udpSocket.send(packet,0,packet.length,3848,HOST_IP)
        }catch(err){
            console.log("连接超时，请检查网络");
            // 超时了
            data();
        }
    })
    
}


/**
 * 保持连接在线。会阻塞线程，保持连接状态status=1，当保持连接失败时，status为0或-1
 * @param session 通过login(byte[] packet);创建得到
 * 
 */
async function breathe(session){
    let state = true;
    return new Promise(async (data,err)=>{
        let aaa = async function aaa(msg, rinfo){
            if(HOST_IP !== rinfo?.address){return}

            // 解密数据
            let recvPacket = decrypt(msg)
            // md5数据
            let recvMd5 = Buffer.alloc(16)
            for (let j = 2; j < 18; j++) {
                recvMd5[j - 2] = recvPacket[j];
                recvPacket[j] = 0;
            }
            if(checkMD5(recvMd5,getMD5Bytes(recvPacket))){
                let statu = recvPacket[20]; // 记录状态,0 或 1， 0表示不在线，1表示在线
                if(statu === 0){
                    udpSocket.removeListener("message",aaa)
    
                    state = false;
                    
                    // 停止心跳
                    clearInterval(bre)
                    // 重新获取ip
                    getLoad();
                    console.log("心跳失败,下线重新请求登录");
                    // 结束阻塞
                    data();
                }
            }else{
                udpSocket.removeListener("message",aaa)
                clearInterval(bre)
                console.log("md5验证失败");
                // 结束阻塞
                data();
            }
        }


        let net = async ()=>{
            if(! state ) return;
            await new Promise(ress=>{ setTimeout(()=>{ress()},5000)})
            while(await isNet()){
                console.log("网络正常")
                await new Promise(ress=>{ setTimeout(()=>{ress()},5000)})
            }
            udpSocket.removeListener("message",aaa)
            clearInterval(bre)
            // 重新获取ip
            getLoad();
            console.log("网络不通,下线重新请求登录");
            // 结束阻塞
            data();
        }
        net();

        udpSocket.on("message",aaa)

        let i = 1;
        var bre = setInterval(()=>{
            try{
                // 生成心跳包数据
                let breathePacket = generateBreathePacket(session);
                // 每隔两秒发送一个心跳包
                udpSocket.send(breathePacket,0,breathePacket.length,3848,HOST_IP,async ()=>{
                    // 心跳成功
                    console.log("持续心跳中...",i++);
                    // 心跳成功，验证一下网络
                })
                index += 3;
            }catch(err){
                udpSocket.removeListener("message",aaa)
                state = false;
                // 停止心跳
                clearInterval(bre)
                // 重新获取ip
                getLoad();
                // 结束阻塞
                data();
            }
        },20000)
    })

}


/**
 * @param {创建保持连接的packet} session 
 */
function generateBreathePacket(session){
    let packet_len = session.length + 88;
    let packet = Buffer.alloc(packet_len);
    let i = -1;
    packet[++i] = 0x03;
    packet[++i] = Buffer.from(String(packet_len));
    for (; i < 17;) {
        packet[++i] = 0;
    }
    packet[++i] = 0x08;
    packet[++i] = Buffer.from(String(session.length + 2));
    for (let b of session) {
        packet[++i] = b;
    }
    packet[++i] = 0x09;
    packet[++i] = 0x12;
    // byte[] bytes = LOCAL_IP.getBytes();
    for (let b of LOCAL_IP) {
        packet[++i] = b.charCodeAt();
    }
    for (let j = 0; j < 16 - LOCAL_IP.length; j++) {
        packet[++i] = 0;
    }
    packet[++i] = 0x07;
    packet[++i] = 0x08;
    let macs = MAC_ADDR.split(":");
    for (let str of macs) {
        packet[++i] = Buffer.from(Number("0x"+str).toString(10));
    }
    packet[++i] = 0x14;
    packet[++i] = 0x06;
    packet[++i] = Buffer.from(String(index >> 24 & 0xff));
    packet[++i] = Buffer.from(String(index >> 16 & 0xff));
    packet[++i] = Buffer.from(String(index >> 8 & 0xff));
    packet[++i] = Buffer.from(String((index & 0xff)));
    for (let b of block) {
        packet[++i] = b;
    }
    let md5Bytes = getMD5Bytes(packet);
    for (let j = 0; j < md5Bytes.length; j++) {
        packet[j + 2] = byte(md5Bytes[j],10);
    }
    packet = encrypt(packet);
    return packet;
}


/**
 * MD5校验，比较两个MD5数据报是否一致
 * @param arg0 第一个MD5数据报 16字节
 * @param arg1 第二个MD5数据报 16字节
 * @return 一致返回true，不一致返回false
 * 
 */
function checkMD5(arg0,arg1){
    let flag = true;
    for (let i = 0; i < arg0.length; i++) {
        if (arg0[i] != arg1[i]) {
            return false;
        }
    }
    return flag;
}




/**
 * 创建连接认证的packet
 * @return byte[] 连接认证的packet
 */
function generateLoginPacket(){
    let packet_len = 38 + USERNAME.length + USERPASS.length + LOCAL_IP.length + SERVICE_TYPE.length
				+ DHCP_SETTING.length + CLIENT_VERSION.length;
    let packet = Buffer.alloc(Number(packet_len))
    let i = -1;
    packet[++i] = 0x01;
    packet[++i] = Buffer.from(String(packet_len));


    for (; i < 17;) {
        packet[++i] = 0;
    }

    packet[++i] = 0x07;
    packet[++i] = 0x08;

    let macs = MAC_ADDR.split(":");

    for (let str of macs) {
        packet[++i] =  Buffer.from(Number("0x"+str).toString(10)+"");
    }


    packet[++i] = 0x01;
    packet[++i] = Buffer.from(String(USERNAME.length + 2));
    for (let b of USERNAME) {
        packet[++i] = b.charCodeAt();
    }


    packet[++i] = 0x02;
    packet[++i] = Buffer.from(String(USERPASS.length + 2));
    for (let b of USERPASS) {
        packet[++i] = b.charCodeAt();
    }


    packet[++i] = 0x09;
    packet[++i] =  Buffer.from(String(LOCAL_IP.length + 2));
    for (let b of LOCAL_IP) {
        packet[++i] = b.charCodeAt();
    }

    packet[++i] = 0x0a;
    packet[++i] = Buffer.from(String(SERVICE_TYPE.length + 2));
    for (let b of SERVICE_TYPE) {
        packet[++i] = b.charCodeAt();
    }


    packet[++i] = 0x0e;
    packet[++i] = Buffer.from(String(DHCP_SETTING.length + 2));
    for (let c of DHCP_SETTING) {
        packet[++i] = Buffer.from(String(c));
    }

    packet[++i] = 0x1f;
    packet[++i] = Buffer.from(String(CLIENT_VERSION.length + 2));
    for (let b of CLIENT_VERSION) {
        packet[++i] = b.charCodeAt();
    }

    let md5Bytes = getMD5Bytes(packet);

    for (let j = 0; j < md5Bytes.length; j++) {
        packet[j + 2] = byte(md5Bytes[j],10);
    }

    packet = encrypt(packet);
    return packet;
}


/**
 * packet加密
 * 
 * @param packet
 *            未加密packet字节数组
 * @return 加密后的packet字节数组
 */
function encrypt(packet){
    let encrypt_packet = Buffer.alloc(packet.length);
    let i = 0;
    for (let b of packet) {
        encrypt_packet[i++] =  Buffer.from(String((b & 0x80) >> 6 | (b & 0x40) >> 4 | (b & 0x20) >> 2 | (b & 0x10) << 2
        | (b & 0x08) << 2 | (b & 0x04) << 2 | (b & 0x02) >> 1 | (b & 0x01) << 7));
    }
    return encrypt_packet;
}

/**
 * packet解密
 * @param packet 加密后的packet字节数组
 * @return 解密后的packet字节数组
 */
 function decrypt(packet){
    let decrypt_packet = Buffer.alloc(packet.length);
		let i = 0;
		for (let b of packet) {
			decrypt_packet[i++] = Buffer.from(String(((b & 0x80) >> 7 | (b & 0x40) >> 2 | (b & 0x20) >> 2 | (b & 0x10) >> 2
            | (b & 0x08) << 2 | (b & 0x04) << 4 | (b & 0x02) << 6 | (b & 0x01) << 1)));
		}
		return decrypt_packet;
}


/**
 * 对byte数组直接进行摘要计算，返回加密后的byte数组
 * @param byteArray 需要进行MD5运算的原始字节数组
 * @return MD5摘要计算后的字节数组
 */
function getMD5Bytes(byteArray){
    let md5Bytes = crypto.createHash("md5").update(byteArray).digest("hex");
    return Buffer.from(md5Bytes,"hex");
}

function getLoad(){
    // 获取ip的mac地址
    let lo = os.networkInterfaces();
    for(let e in lo){
        for(let ee of lo[e]){
            if(!/^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$/.test(ee.address) || ["127.0.0.1","0.0.0.0"].includes(ee.address)) continue;
            IP.push({
                ip:ee.address,
                mac:ee.mac
            })
        }
    }
    if(IP.length === 0){
        console.log("没找到电脑中的可用ip，你要连接了网络才能登录")
        exit(0);
    }
    LOCAL_IP = IP[0].ip;
    MAC_ADDR = IP[0].mac;
    IP.splice(0,1);
}


/**
 * @param {*} str 
 * @returns
 */
 function byte(str,number=16){
    if(number === 10) str = Number(str).toString(16);
    str = Number("0x"+str).toString(2);
    // 不足8位为正数，直接返回
    if(str.length<8) return Number("0b"+str).toString(10);
    // 足8位，为负数，进行反码和补码
    str = Array.from(str.substring(1)).map(e=>e==1?0:1).join("");
    // 二进制转换成10进制
    return ~Number("0b"+str).toString(10)+""
}

/**
 * 
 * @param {二进制转gbk} buff 
 * @returns 
 */
function gbk(buff){
    return new TextDecoder("gbk") .decode(buff)
}



async function logout(session){
    return new Promise((data)=>{
        udpSocket.once("message",()=>{data()})
        index += 3;
        let downnetPacket = generateDownnetPacket(session);
        udpSocket.send(downnetPacket,0,downnetPacket.length,3848,HOST_IP);
    })
}

// 创建请求下线的packet
function generateDownnetPacket(session){
    let packet_len = session.length + 88;
    let packet = Buffer.alloc(packet_len);
    let i = -1;
    packet[++i] = 0x05;
    packet[++i] = Buffer.from(String(packet_len));
    for (; i < 17;) {
        packet[++i] = 0;
    }
    packet[++i] = 0x08;
    packet[++i] = Buffer.from(String(session.length + 2));
    for (let b of session) {
        packet[++i] = b;
    }
    packet[++i] = 0x09;
    packet[++i] = 0x12;
    // byte[] bytes = LOCAL_IP.getBytes();
    for (let b of LOCAL_IP) {
        packet[++i] = b.charCodeAt();
    }
    for (let j = 0; j < 16 - LOCAL_IP.length; j++) {
        packet[++i] = 0;
    }
    packet[++i] = 0x07;
    packet[++i] = 0x08;
    let macs = MAC_ADDR.split(":");
    for (let str of macs) {
        packet[++i] = Buffer.from(Number("0x"+str).toString(10));
    }
    packet[++i] = 0x14;
    packet[++i] = 0x06;
    let indexStr = index.toString(16);
    let indexLen = indexStr.length;

    packet[++i] = Buffer.from(Number("0x"+indexStr.substring(0, indexLen - 6)).toString(10));
    packet[++i] = Buffer.from(Number("0x"+indexStr.substring(indexLen - 6, indexLen - 4)).toString(10));
    packet[++i] = Buffer.from(Number("0x"+indexStr.substring(indexLen - 4, indexLen - 2)).toString(10));
    packet[++i] = Buffer.from(Number("0x"+indexStr.substring(indexLen - 2, indexLen - 0)).toString(10));
    for (let b of block) {
        packet[++i] = b;
    }
    let md5Bytes = getMD5Bytes(packet);
    for (let j = 0; j < md5Bytes.length; j++) {
        packet[j + 2] = byte(md5Bytes[j],10);
    }
    packet = encrypt(packet);
    return packet;
}


// 读取控制器输出的数据
function readlineSync() {
    return new Promise((resolve, reject) => {
        process.stdin.resume();
        process.stdin.on('data', function (data) {
            process.stdin.pause(); // 在读取一行后停止
            resolve(data.replace(/[\r\n]/g,""));
            
        });
    });
}


/**
 * 检查网络是否连通
 */
async function isNet(){
    let i = 0,state = false;
    // 有3次测试网络的失败次数
    while(i<3){
        await new Promise(ress=>{
            http.get("http://www.msftconnecttest.com/connecttest.txt",res=>{
                    let data = ""
                    res.on("data",(a)=>data+=a)
                    res.on("end",_=>{
                        if(data === "Microsoft Connect Test"){state = true; ress(true); i = 3}
                        else{i++}
                    })
                    res.on("error",()=>{console.log("res err"); ress(true); i++})
                }).on("error",_=>{console.log("req err"); ress(true); i++})
        })
    }
    return state;
}
